#!/bin/bash
####### mugsyback.sh #######
set -u
#set -e

# redirect echo to a file and ignore sdout from commands
# create a function for this use file descriptor or perhaps just
# straight redirection?
# look at the double variable thing for the global variables
MUGSYBACK_CONF="/etc/mugsyback.conf"
DEBUG="false"
VERBOSE="false"
readonly SCRIPTNAME="$(basename ${0})"
readonly BACKUP_NAME_FORMAT="$(date +%F-%s)"
readonly LOGFILE="/tmp/${SCRIPTNAME}.log"
readonly LOG_REPORT="/tmp/${SCRIPTNAME}-report.log"
> "${LOG_REPORT}"
#exec 2>&1 | tee -a ${LOGFILE}
#exec > >(tee -a ${LOGFILE}) 2>&1
#exec 1> /tmp/${SCRIPTNAME}-stdout.log
#exec 2> /tmp/${SCRIPTNAME}-stderr.log
#exec 1> >(tee -a /tmp/${SCRIPTNAME}-stdout.log)
#exec 2> >(tee -a /tmp/${SCRIPTNAME}-stderr.log)
#exec 3>$LOGFILE
#exec > ${LOGFILE} 2>&1

usageAndBail()
# Purpose:  print usage and bail
{
    cat << EOT
Usage: mugsyback [OPTION]...

   -c   config file path (default /etc/mugsyback.conf)
   -d   enable debugging mode
   -e   enable email reports
   -l   also log to stdout

EOT
        exit 1 ; 
}

main()
# Purpose:  main execution function
{
    local total_s_time="$(date +%s)"

    ####### command line arguments
    while getopts "c:dvel" o ; do
            case "$o" in

            # problem with this receiving full path if
            # relative is used
                    c)      MUGSYBACK_CONF="$(readlink -f ${OPTARG})" ;;
                    d)      DEBUG="true" ; set -x ;;
                    v)      VERBOSE="true" ;;
                #   d)      DEBUG="true" ;;
                    e)      enableEmail=1 ;;
                    l)      logToStandardOut=-s ;;
                    :)      echo "Option -${OPTARG} requires an argument." ; exit 1 ;;
                    *)      usageAndBail ;;
            esac
    done

    sanityChecks || exit 1
    # get global script vars (local to main only)
    eval $(sed -e '/^ *#/d;s/#.*//' -e '/^$/d' -e "0,/\[DefaultVariables\]/d" -e "/\[.*\]/,\$d" -e 's/^/local /' ${MUGSYBACK_CONF})
    eval $(sed -e '/^ *#/d;s/#.*//' -e '/^$/d' -e "0,/\[ScriptVariables\]/d" -e "/\[.*\]/,\$d" -e 's/^/local /' ${MUGSYBACK_CONF})
    local backupSourceList="$(sed -e '/^ *#/d;s/#.*//' -e '/^$/d' ${MUGSYBACK_CONF} | grep "\[[0-99]\]" | tr -d '[]' | sort)"

    for i in $(echo ${backupSourceList}) ; do
        # function used to keep variables contained
        backupLoop "${i}"
    done

    echo "=== BACKUP STATISTICS ===" >> "${LOG_REPORT}"
    local total_e_time="$(date +%s)"
    local total_duration="$(echo $((total_e_time - total_s_time)))"
    if [[ "${total_duration}" -le "60" ]] ; then
        echo "Total Duration: ${total_duration} Seconds" >> "${LOG_REPORT}"
    else
        echo "Total Duration: $(awk 'BEGIN{ printf "%.2f\n", '${total_duration}'/60}') Minutes" >> "${LOG_REPORT}"
    fi

}

backupLoop()
{
    local backup_source_num="${1}" ; shift

    eval $(sed -e '/^ *#/d;s/#.*//' -e '/^$/d' -e "0,/\[${backup_source_num}\]/d" -e "/\[.*\]/,\$d" -e 's/^/local /' ${MUGSYBACK_CONF})

    local DESTINATION="${DESTINATION:=${DEFAULT_DESTINATION}}"
    local BACKUP_DIR="${BACKUP_DIR:=${DEFAULT_BACKUP_DIR}}"
    local CHECKFILE="${CHECKFILE:=${DEFAULT_CHECKFILE}}"
    local ROTATION_COUNT="${ROTATION_COUNT:=${DEFAULT_ROTATION_COUNT}}"
    local SOURCE="${SOURCE:=${DEFAULT_SOURCE}}"
    local ENGINE="${ENGINE:=${DEFAULT_ENGINE}}"
    local ENGINE_OPTIONS="${ENGINE_OPTIONS:=${DEFAULT_ENGINE_OPTIONS}}"
    local TAR_EXTENSION="${TAR_EXTENSION:=${DEFAULT_TAR_EXTENSION}}"
    local EXCLUDE="${EXCLUDE:- }"
    local LVM_SNAPSHOT="${LVM_SNAPSHOT:=${DEFAULT_LVM_SNAPSHOT}}"
    local LVM_VG="${LVM_VG:=${DEFAULT_LVM_VG}}"
    local LVM_SNAPSHOT_SIZE="${LVM_SNAPSHOT_SIZE:=${DEFAULT_LVM_SNAPSHOT_SIZE}}"
    local LVM_SNAPSHOT_MNT_OPTS="${LVM_SNAPSHOT_MNT_OPTS:-ro}"
    [[ "${LVM_SNAPSHOT}" = "true" ]] && local source_path="/mnt/${LVM_NAME}-snapshot${SOURCE}" || local source_path="${SOURCE}"

    echo "=== BACKUP REPORT FOR \"${NAME}\" ===" >> "${LOG_REPORT}"

    destinationMounted && local destination_already_mounted="true" || local destination_already_mounted="false"

    # has to be located here to receive variables   
    trap "cleanup ; break" INT TERM ERR

    if [[ "${destination_already_mounted}" == "false" ]] ; then
        mountBackupDestination || { logReport "FAILURE" ; continue ; }
    fi

    checkBackupDestination || { logReport "FAILURE" ; continue ; }

    if [[ "${LVM_SNAPSHOT}" = "true" ]] ; then
        lvmCreateSnapshot || { umountBackupDestination ; logReport "FAILURE" ; continue ; }
        lvmMountSnapshot || { lvmRemoveSnapshot ; umountBackupDestination ; logReport "FAILURE" ; continue ; }
    fi

    local s_time="$(date +%s)"
    backupUsing${ENGINE} && post_engine_cleanup="0" || post_engine_cleanup="1"
    local e_time="$(date +%s)"
    local engine_duration="$(echo $((e_time - s_time)))"

    if [[ "${LVM_SNAPSHOT}" = "true" ]] ; then
        lvmUmountSnapshot || post_engine_cleanup="1"
        lvmRemoveSnapshot || post_engine_cleanup="1"
    fi

    backup_disk_usage_percent="$(df -Ph ${DESTINATION} | tail -1 | awk '{print $5}')"
    backup_disk_usage_remaining="$(df -Ph ${DESTINATION} | tail -1 | awk '{print $4}')"

    umountBackupDestination || post_engine_cleanup="1"
    
    [[ "${post_engine_cleanup}" == "0" ]] && logReport "SUCCESS" || logReport "FAILURE"

    return "${post_engine_cleanup}"
}

cleanup()
# Purpose:  cleanup
{
    if [[ "${LVM_SNAPSHOT}" = "true" ]] ; then
        lvmUmountSnapshot 
        lvmRemoveSnapshot
    fi
    umountBackupDestination
    logReport "FAILURE"
}

#######
# Logging
#######

#Perhaps add logStdOut and logStdErr (replace logFile)

logFile()
{
    echo "${@}" >> $LOGFILE
}

logVerbose()
{
    if [[ "${VERBOSE}" == "true" ]] ; then
        echo "${@}"
        logFile "${@}"
    fi
}

logDebug()
{
    if [[ "${DEBUG}" == "true" ]] ; then
        echo "DEBUG: ${@}"
        logFile "DEBUG: ${@}"
    fi
}

logError()
{
    echo "ERROR: ${@}"
    logFile "ERROR: ${@}"
}

logReport()
{
    local backup_result="${1}" ; shift
    local backup_dir_path="${DESTINATION}/${BACKUP_DIR}/${NAME}"
    [[ ${ENGINE} = "tar" ]] && local backup_name=${BACKUP_NAME_FORMAT}.${TAR_EXTENSION}
    [[ ${ENGINE} = "rsync" ]] && local backup_name=${BACKUP_NAME_FORMAT}

    echo "Backup Status: ${backup_result}" >> "${LOG_REPORT}"
    echo "Backup Engine: ${ENGINE}" >> "${LOG_REPORT}"
    echo "Backup Source: ${source_path}" >> "${LOG_REPORT}"
    echo "Backup Destination: ${backup_dir_path}/${backup_name}" >> "${LOG_REPORT}"
    echo "Backup Disk Usage: ${backup_disk_usage_percent}, ${backup_disk_usage_remaining} Remaining" >> "${LOG_REPORT}"
    if [[ "${engine_duration}" -le "60" ]] ; then
        echo "Backup Duration: ${engine_duration} Seconds" >> "${LOG_REPORT}"
    else
        echo "Backup Duration: $(awk 'BEGIN{ printf "%.2f\n", '${engine_duration}'/60}') Minutes" >> "${LOG_REPORT}"
    fi
    echo "" >> "${LOG_REPORT}"
}


syncDisks()
# Purpose:  force changed blocks to disk
{
    cd / ; sync & wait
}

destinationMounted()
# Purpose:  check if backup destination is already mounted
{
    [[ "$(grep "${DESTINATION}" /proc/mounts)" ]] \
        && { logVerbose "${DESTINATION} already mounted" ; return 0 ; } \
        || { logVerbose "${DESTINATION} not mounted" ; return 1 ; }
}

mountBackupDestination()
# Purpose:  mount backup destination
{
    syncDisks ; mount "${DESTINATION}" \
        && { logVerbose "${DESTINATION} mounted successfully" ; return 0 ; } \
        || { logError "${DESTINATION} mount failed cannot continue" ; return 1 ; }
}

umountBackupDestination()
# Purpose:  un-mount backup destination
{
    syncDisks
    if [[ "${destination_already_mounted}" = "false" ]] ; then
        umount "${DESTINATION}" \
            && { logVerbose "${DESTINATION} un-mounted succesfully" ; return 0 ; } \
            || { logError "${DESTINATION} un-mount failed" ; return 1 ; }
    fi
}

checkBackupDestination()
# Purpose:      check backup destination is prepared
{           
    [[ -f "${DESTINATION}/${BACKUP_DIR}/${CHECKFILE}" ]] \
        && { logVerbose "${DESTINATION}/${BACKUP_DIR}/${CHECKFILE} exists, continuing..." ; return 0 ; } \
        || { logError "${DESTINATION}/${BACKUP_DIR}/${CHECKFILE} does not exist, cannot proceed!" ; return 1 ; }
}

lvmCreateSnapshot()
# Purpose:  create lvm snapshot
# https://bugzilla.redhat.com/show_bug.cgi?id=577798
# http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=618016
{
    local lvm_snapshot_name="${LVM_NAME}-snapshot"
    syncDisks ; lvcreate -s -l +${LVM_SNAPSHOT_SIZE}%FREE -n ${lvm_snapshot_name} ${LVM_VG}/${LVM_NAME} 1> /dev/null \
        && { logVerbose "${LVM_NAME} snapshot created successfully" ; return 0 ; } \
        || { logError "${LVM_NAME} snapshot creation failed" ; return 1 ; }
}

lvmMountSnapshot()
# Purpose:  mount lvm snapshot
{
    local lvm_snapshot_name="${LVM_NAME}-snapshot"

    [[ -d "/mnt/${lvm_snapshot_name}" ]] \
        && logVerbose "/mnt/${lvm_snapshot_name} already exists." \
        || mkdir -p /mnt/${lvm_snapshot_name}
    
    syncDisks ; mount -o ${LVM_SNAPSHOT_MNT_OPTS} /dev/${LVM_VG}/${lvm_snapshot_name} /mnt/${lvm_snapshot_name} \
        && { logVerbose "${lvm_snapshot_name} mounted successfully" ; return 0 ; } \
        || { logError "${lvm_snapshot_name} mount failed" ; return 1 ; }
}

lvmUmountSnapshot()
# Purpose:  un-mount lvm snapshot
{
    local lvm_snapshot_name="${LVM_NAME}-snapshot"
    syncDisks ; umount /mnt/${lvm_snapshot_name} \
        && { logVerbose "${lvm_snapshot_name} un-mounted successfully" ; return 0 ; } \
        || { logError "${lvm_snapshot_name} un-mount failed" ; return 1 ; }
}

lvmRemoveSnapshot()
# Purpose:  remove lvm Snapshot
{
    local lvm_snapshot_name="${LVM_NAME}-snapshot"
    syncDisks ; lvremove --force ${LVM_VG}/${lvm_snapshot_name} 1> /dev/null \
        && { logVerbose "${LVM_NAME} snapshot removed successfully" ; return 0 ; } \
        || { logError "${LVM_NAME} snapshot removal failed" ; return 1 ; }
}

backupRotation()
# Purpose:  rotate backup folders
{
    local backup_dir_path="${DESTINATION}/${BACKUP_DIR}/${NAME}"
    local list_backups=$(ls -c "${backup_dir_path}")
    local backups_to_keep=$(ls -c "${backup_dir_path}" | head -"${ROTATION_COUNT}")

    OLDIFS="${IFS}"
    IFS='
'
    for i in ${list_backups} ; do
        local found="0"

        for j in ${backups_to_keep} ; do
            [[ ${i} = ${j} ]] && local found="1"
        done

        if [[ "${found}" -eq "0" ]] ; then
            rm -rf "${backup_dir_path}/${i}" \
                && logVerbose "Removed ${backup_dir_path}/${i}" \
                || { logError "Failed to remove ${backup_dir_path}/${i}, your backup dirs may need tidyup." ; IFS="${OLDIFS}" ; return 1 ; }
        fi
    done

    IFS="${OLDIFS}"
    return 0
}

sanityChecks()
# i hate this...... hacky rubbish
{
    [[ -s "${MUGSYBACK_CONF}" ]] || { logError "${MUGSYBACK_CONF}" not found ; return 1 ; }
    type rsync > /dev/null || { echo "program \"rsync\" not found is it installed?" ; exit 1 ; }
    # might remove this if i can fugure out tar naming.
    type file > /dev/null || { echo "program \"file\" not found is it installed?" ; exit 1 ; }

    # check that backupSources are unique.
    local backup_sources="/tmp/${SCRIPTNAME}-backupsources.txt"
    > "${backup_sources}"
    for i in $(sed -e '/^ *#/d;s/#.*//' -e '/^$/d' -e '/^\[backupSource.*]$/!d' ${MUGSYBACK_CONF}) ; do
        echo "${i}" >> "${backup_sources}"
    done

    [[ -z "$(uniq -d "${backup_sources}")" ]] || { echo "duplicate backup source found, check your config file." ; return 1 ; }

    # check that $NAME is not set the same in backup sources
    local names="/tmp/${SCRIPTNAME}-names.txt"
    (IFS='
'
    > "${names}"
    for i in $(sed -e '/^ *#/d;s/#.*//' -e '/^$/d' -e '/^NAME=/!d' -e 's/^/local /' ${MUGSYBACK_CONF}) ; do
        eval "${i}"
        echo "${NAME}" >> "${names}"
    done)

    [[ -z "$(uniq -d "${names}")" ]] || { echo "duplicate NAME variable found in backup sources, check your config file." ; return 1 ; }

    # return 0 if we get this far
    logVerbose "sanity checks passed" ; return 0
}

backupUsingrsync()
# Purpose:  use rsync as backup engine
{
    local backup_dir_path="${DESTINATION}/${BACKUP_DIR}/${NAME}"
    local rsync_exclusions=$(IFS=, ; for i in ${EXCLUDE} ; do printf "%s" "--exclude=\"${i}\" " ; done)

    # logic for folder creation - mkdir without -p is important
    [[ -d "${backup_dir_path}" ]] || mkdir "${backup_dir_path}"

    [[ "$(ls -A ${backup_dir_path})" ]] \
        && { logVerbose "\"${backup_dir_path}\" exists and contains folders" ; rsyncLinkDest=$(ls -c -1 "${backup_dir_path}/" | head -1) ; } \
        || { mkdir "${backup_dir_path}/${BACKUP_NAME_FORMAT}" ; rsyncLinkDest="${BACKUP_NAME_FORMAT}" ; }

    # destination folder is created before backupRotation runs to keep
    # rotation number correct.
    # might need IFS as dirs may have spaces?
    # logic for rsync if using 1 backup rotation
    if [[ "${ROTATION_COUNT}" -eq "1" ]] ; then 
        [[ "${rsyncLinkDest}" != "${BACKUP_NAME_FORMAT}" ]] && mv "${backup_dir_path}/${rsyncLinkDest}" "${backup_dir_path}/${BACKUP_NAME_FORMAT}"
    else
        [[ -d "${backup_dir_path}/${BACKUP_NAME_FORMAT}" ]] || mkdir "${backup_dir_path}/${BACKUP_NAME_FORMAT}"
    fi  

    backupRotation "${DESTINATION}" "${BACKUP_DIR}" "${NAME}" "${ROTATION_COUNT}" || return 1

    [[ "${ROTATION_COUNT}" -ge "2" ]] && local ENGINE_OPTIONS="${ENGINE_OPTIONS} --link-dest=${backup_dir_path}/${rsyncLinkDest}"

    cd "${source_path}" || { logError "failed to change directory into ${source_path}, check config file" && return 1 ; }

    eval rsync "${rsync_exclusions}" "${ENGINE_OPTIONS}" ./ "${backup_dir_path}/${BACKUP_NAME_FORMAT}" \
        && { logVerbose "${NAME} backup completed successfully" ; syncDisks ; local rsync_engine_status="0" ; } \
        || { logError "${NAME} backup failed" ; syncDisks ; local rsync_engine_status="1" ; }

    return ${rsync_engine_status}
}

backupUsingtar()
# Purpose:  use tar as backup engine
{
    local backup_dir_path="${DESTINATION}/${BACKUP_DIR}/${NAME}"
    local tar_exclusions=$(IFS=, ; for i in ${EXCLUDE} ; do printf "%s" "--exclude=\"${i}\" " ; done)
    local tar_archive_name=${BACKUP_NAME_FORMAT}.${TAR_EXTENSION}

    # logic for file creation - mkdir without -p is important
    [[ -d "${backup_dir_path}" ]] || mkdir "${backup_dir_path}"
    touch "${backup_dir_path}/${tar_archive_name}"

    backupRotation "${DESTINATION}" "${BACKUP_DIR}" "${NAME}" "${ROTATION_COUNT}" || return 1
    
    cd "${source_path}" || { echo "failed to change directory into ${source_path}, check config file" ; return 1 ; }

    eval tar "${ENGINE_OPTIONS}" "${backup_dir_path}/${tar_archive_name}" ./ "${tar_exclusions}" \
        && { logVerbose "${NAME} backup completed successfully" ; syncDisks ; local tar_engine_result="0" ; } \
        || { logError "${NAME} backup failed" ; syncDisks ; local tar_engine_result="1" ; }

    return "${tar_engine_result}" 
}

###### start script

main "${@}"

####### END #######

